<?php

namespace maoxuner\LaravelJsonRpc\Client;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use maoxuner\LaravelJsonRpc\Entities\RequestObject;
use maoxuner\LaravelJsonRpc\Entities\ResponseObject;
use maoxuner\LaravelJsonRpc\Exceptions\NonRpcException;
use maoxuner\LaravelJsonRpc\Exceptions\ParseException;
use maoxuner\LaravelJsonRpc\Exceptions\ServerException;

class Client
{
    protected string $protocol = '2.0';

    protected PendingRequest $client;

    protected bool $batchMode = false;

    protected array $requests = [];

    protected mixed $errorHandler = null;

    public function __construct(PendingRequest $client)
    {
        $this->client = $client->asJson()->acceptJson();
    }

    /**
     * @return null
     *
     * @throws ParseException
     * @throws ServerException
     */
    public function batch(callable $callback)
    {
        $this->batchMode = true;
        $callback($this);
        $this->batchMode = false;

        if (empty($this->requests)) {
            throw new ParseException;
        } else {
            $requests = $this->requests;
            $this->requests = [];
        }

        return $this->send($requests, true);
    }

    public function execute(string $method, array $params = [], ?string $id = null)
    {
        return $this->request($method, $params, $id ?: Str::uuid()->toString());
    }

    public function notify(string $method, array $params = []): void
    {
        $this->request($method, $params);
    }

    /**
     * @return static|mixed
     *
     * @throws ServerException
     */
    protected function request(string $method, array $params = [], ?string $id = null): mixed
    {
        $jsonrpc = $this->protocol;
        $data = compact(
            'jsonrpc',
            'method',
            'params',
            'id'
        );

        if ($this->batchMode) {
            $this->requests[] = $data;

            return $this;
        } else {
            return $this->send($data);
        }
    }

    protected function send(array $data, bool $batchMode = false)
    {
        $requests = collect($batchMode ? $data : [$data])
            ->map([RequestObject::class, 'load']);

        $response = $this->client->post(
            '',
            $batchMode
                ? $requests->toArray()
                : $requests->first()->toArray()
        );

        if (! $response->successful()) {
            throw new ServerException($response->reason());
        }

        $result = $response->json();
        if (empty($result)) {
            return null;
        }

        $responses = collect($batchMode && Arr::isList($result) ? $result : [$result])
            ->map([ResponseObject::class, 'load']);

        $results = $responses->map(function (ResponseObject $response) {
            if ($response->error) {
                $exception = $response->error->exception();
                if (
                    $exception instanceof NonRpcException
                    && is_callable($this->errorHandler)
                ) {
                    return call_user_func($this->errorHandler, $exception);
                }

                throw $exception;
            }

            return $response->result;
        });

        return $batchMode ? $results->toArray() : $results->first();
    }

    public function catch(?callable $callback = null): static
    {
        $this->errorHandler = $callback;

        return $this;
    }
}
